using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace Drill.Core.Utilities
{
    /// <summary>
    /// A drop-in replacement for the <see cref="Path"/> class that provides additional features.
    /// </summary>
    public static class PathUtility
    {
        #region " Static Constructor "

        static PathUtility()
        {
            _altDirectorySeparatorChar = Path.AltDirectorySeparatorChar;
            _directorySeparatorChar = Path.DirectorySeparatorChar;
            _pathSeparator = Path.PathSeparator;
            _volumeSeparatorChar = Path.VolumeSeparatorChar;

            _altDirectorySeparator = AltDirectorySeparatorChar.ToString(CultureInfo.InvariantCulture);
            _directorySeparator = DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture);
            _invalidPathCharacters = GetInvalidPathChars();
            _pathSeparatorString = _pathSeparator.ToString(CultureInfo.InvariantCulture);
            _volumeSeparator = _volumeSeparatorChar.ToString(CultureInfo.InvariantCulture);
            _webDirectorySeparator = WebDirectorySeparatorChar.ToString(CultureInfo.InvariantCulture);
        }

        #endregion " Static Constructor "

        #region " Constants "

        public const string CurrentDirectoryRelativePathPart = ".";
        public const string ParentDirectoryRelativePathPart = "..";
        private const char   WebDirectorySeparatorCharacter = '/';

        #endregion " Constants "

        #region " Fields "

        private static readonly string _altDirectorySeparator;
        private static readonly char   _altDirectorySeparatorChar;
        private static readonly string _directorySeparator;
        private static readonly char   _directorySeparatorChar;
        private static readonly char[] _invalidPathCharacters;
        private static readonly char   _pathSeparator;              // Not named _pathSeparatorChar to be consistent with System.IO.Path's PathSeparator field.
        private static readonly string _pathSeparatorString;
        private static readonly string _volumeSeparator;
        private static readonly char   _volumeSeparatorChar;
        private static readonly string _webDirectorySeparator;

        #endregion " Fields "

        #region " Properties "

        public static string AltDirectorySeparator
        {
            get { return _altDirectorySeparator; }
        }

        public static char AltDirectorySeparatorChar
        {
            get { return _altDirectorySeparatorChar; }
        }

        public static string DirectorySeparator
        {
            get { return _directorySeparator; }
        }

        public static char DirectorySeparatorChar
        {
            get { return _directorySeparatorChar; }
        }

        public static char PathSeparator
        {
            get { return _pathSeparator; }
        }

        public static string PathSeparatorString
        {
            get { return _pathSeparatorString; }
        }

        public static string VolumeSeparator
        {
            get { return _volumeSeparator; }
        }

        public static char VolumeSeparatorChar
        {
            get { return _volumeSeparatorChar; }
        }

        public static string WebDirectorySeparator
        {
            get { return _webDirectorySeparator; }
        }

        public static char WebDirectorySeparatorChar
        {
            get { return WebDirectorySeparatorCharacter; }
        }

        #endregion " Properties "

        #region " Methods "

        #region " System.IO.Path Method Wrappers "

        public static string Combine(params string[] paths)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.Combine(paths);
        }

        public static string Combine(string path1, string path2)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.Combine(path1, path2);
        }

        public static string Combine(string path1, string path2, string path3)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.Combine(path1, path2, path3);
        }

        public static string Combine(string path1, string path2, string path3, string path4)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.Combine(path1, path2, path3, path4);
        }

        public static string GetDirectoryName(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetDirectoryName(path);
        }

        public static string GetExtension(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetExtension(path);
        }

        public static string GetFileName(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetFileName(path);
        }

        public static string GetFileNameWithoutExtension(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetFileNameWithoutExtension(path);
        }

        public static string GetFullPath(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetFullPath(path);
        }

        public static char[] GetInvalidFileNameChars()
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetInvalidFileNameChars();
        }

        public static char[] GetInvalidPathChars()
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetInvalidPathChars();
        }

        public static string GetPathRoot(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetPathRoot(path);
        }

        public static string GetRandomFileName()
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetRandomFileName();
        }

        public static string GetTempFileName()
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetTempFileName();
        }

        public static string GetTempPath()
        {
            // Delegate to the System.IO.Path implementation.
            return Path.GetTempPath();
        }

        public static bool HasExtension(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.HasExtension(path);
        }

        public static bool IsPathRooted(string path)
        {
            // Delegate to the System.IO.Path implementation.
            return Path.IsPathRooted(path);
        }

        #endregion " System.IO.Path Method Wrappers "

        public static bool ContainsInvalidCharacters(string path)
        {
            return ContainsSpecifiedCharacters(path, _invalidPathCharacters);
        }

        public static bool ContainsSpecifiedCharacters(string path, char[] specifiedCharacters)
        {
            if (null == specifiedCharacters)
            {
                throw new ArgumentNullException("specifiedCharacters");
            }

            if (specifiedCharacters.Length < 1)
            {
                throw new ArgumentException("The specifiedCharacters parameter cannot be empty. It must contain at least one char.", "specifiedCharacters");
            }

            if (false == string.IsNullOrEmpty(path))
            {
                return (path.IndexOfAny(specifiedCharacters) > -1);
            }
            return false;
        }

        public static bool EndsWithDirectorySeparator(string path)
        {
            ValidatePath(path);

            return path.EndsWith(DirectorySeparator);
        }

        public static string EnsurePathEndsWithDirectorySeparator(string path)
        {
            return EnsurePathEndsWithDirectorySeparator(path, DirectorySeparator);
        }

        public static string EnsurePathEndsWithDirectorySeparator(string path, string directorySeparator)
        {
            ValidatePath(path);

            ArgumentException argumentException =
                ValidationUtility.GetArgumentExceptionForStringIsNullOrIsEmptyOrContainsOnlyWhitespace(directorySeparator, "directorySeparator");
            if (null != argumentException)
            {
                throw argumentException;
            }

            string workPath = path.TrimEnd();

            if (false == workPath.EndsWith(directorySeparator))
            {
                workPath += directorySeparator;
            }

            return workPath;
        }

        public static string EnsurePathEndsWithDirectorySeparatorChar(string path, char directorySeparatorChar)
        {
            string directorySeparator = directorySeparatorChar.ToString(CultureInfo.InvariantCulture);
            return EnsurePathEndsWithDirectorySeparator(path, directorySeparator);
        }

        /// <summary>
        /// Determines whether the specified path is at the root level.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>
        /// 	<c>true</c> if the specified path is at the root level; otherwise, <c>false</c>.
        /// </returns>
        /// <remarks>
        ///     If the path is not rooted, <c>false</c> will be returned.
        /// </remarks>
        public static bool IsPathRoot(string path)
        {
            ValidatePath(path);

            if (IsPathRooted(path))
            {
                string workPath = path;
                string root = GetPathRoot(workPath);
                if (EndsWithDirectorySeparator(root))
                {
                    workPath = EnsurePathEndsWithDirectorySeparator(workPath);
                }

                return (0 == String.Compare(root, workPath, StringComparison.OrdinalIgnoreCase));
            }

            return false;
        }

        public static bool IsPathValid(string path)
        {
            return ValidatePathImpl(path, false);
        }

        public static string GetLastPathPart(string path)
        {
            ValidatePath(path);

            string[] pathParts = GetPathParts(path);

            return pathParts[pathParts.Length - 1];
        }

        public static string GetParentPath(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                return path;
            }

            string workingPath = path.TrimEnd();

            if (EndsWithDirectorySeparator(workingPath))
            {
                workingPath = workingPath.Substring(0, workingPath.Length - DirectorySeparator.Length);
            }

            int i = workingPath.LastIndexOf(DirectorySeparator, StringComparison.Ordinal);
            if (i > -1)
            {
                return workingPath.Substring(0, i).TrimEnd();
            }

            return path;
        }

        #region " Resolve Relative Path Parts Methods "

        /// <summary>
        /// Resolves the relative path parts including parent path elements within the specified physical path.
        /// </summary>
        /// <param name="path">The path to resolve.</param>
        /// <returns>A path with all relative path parts resolved.</returns>
        public static string ResolveRelativePathParts(string path)
        {
            return ResolveRelativePathParts(path, DirectorySeparatorChar);
        }

        /// <summary>
        /// Resolves the relative path parts including parent path elements (..) within the specified physical path.
        /// </summary>
        /// <param name="path">The path to resolve.</param>
        /// <param name="directorySeparator">The directory separator character.</param>
        /// <returns>An absolute path with all relative path parts resolved.</returns>
        /// <remarks>Resolves "." and ".." relative path parts in the specified path returning an absolute path.</remarks>
        public static string ResolveRelativePathParts(string path, char directorySeparator)
        {
            if (string.IsNullOrEmpty(path))
            {
                return path;
            }

            // workPath is a copy of path that will be processed within this method.
            string workPath = path;

            // An array of relative path parts to be resolved.
            string[] relativePathParts = new[] { CurrentDirectoryRelativePathPart, ParentDirectoryRelativePathPart };

            // Normalize the directory separator character in workPath.
            workPath = workPath.Replace('/', directorySeparator);
            workPath = workPath.Replace('\\', directorySeparator);  // NOTE: '\\' is the escaped form of the '\' char.

            // Tokenize the path parts
            string[] pathParts = workPath.Split(new[] {directorySeparator}, StringSplitOptions.None);

            int lastIndex = pathParts.Length - 1;

            // Create a string list to hold the path parts
            List<string> list = new List<string>(pathParts.Length);

            for (int i = 0; i < pathParts.Length; i++)
            {
                string pathPart = pathParts[i];
                if (false == string.IsNullOrEmpty(pathPart))
                {
                    bool foundRelativePathPart = false;

                    // Determine if the current pathPart is a relative path part.
                    foreach (string relativePathPart in relativePathParts)
                    {
                        if (pathPart == relativePathPart)
                        {
                            // The current path part is a relative path part.
                            // Handle it.
                            switch (pathPart)
                            {
                                case CurrentDirectoryRelativePathPart:
                                    // Add nothing to the list to remove this token from the path.
                                    foundRelativePathPart = true;
                                    break;

                                case ParentDirectoryRelativePathPart:
                                    foundRelativePathPart = true;
                                    if (list.Count < 1)
                                    {
                                        throw new ArgumentException(string.Format("The path '{0}' could not be resolved because the path begins with a parent path '..' part.", path), "path");
                                    }
                                    // Remove the last part from the list.
                                    list.RemoveAt(list.Count - 1);
                                    break;
                            }
                        }

                        if (foundRelativePathPart)
                        {
                            break;
                        }
                    }

                    if (false == foundRelativePathPart)
                    {
                        // The current path part is not a relative path part.
                        // Add it to the list.
                        list.Add(pathPart);
                    }
                }
                else
                {
                    // If the last part is null or empty, do not add it.
                    if (i < lastIndex)
                    {
                        list.Add(string.Empty);
                    }
                }
            }

            // Determine if the last character in workPath is a directory separator char.
            bool pathEndsWithDirectorySeparator = (workPath.Substring(workPath.Length - 1, 1).ToCharArray()[0] == directorySeparator);

            // Create a StringBuilder to construct the new path.
            StringBuilder sb = new StringBuilder(path.Length);

            lastIndex = list.Count - 1;

            for (int i = 0; i < list.Count; i++)
            {
                string pathPart = list[i];
                if (false == string.IsNullOrEmpty(pathPart))
                {
                    sb.Append(pathPart);
                }

                if (i < lastIndex || (i == lastIndex && pathEndsWithDirectorySeparator))
                {
                    sb.Append(directorySeparator);
                }
            }

            if (pathEndsWithDirectorySeparator && sb.ToString().Equals(string.Empty))
            {
                sb.Append(directorySeparator);
            }

            workPath = sb.ToString();

            if (Path.IsPathRooted(path) && false == Path.IsPathRooted(workPath))
            {
                throw new Exception(string.Format("The rooted path '{0}' could not be resolved correctly because the resolved path is not rooted. This usually occurs when parent path parts '..' traverse up too many levels.", path));
            }

            return workPath;
        }

        /// <summary>
        /// Resolves the relative path parts including parent path elements within the specified physical path.
        /// </summary>
        /// <param name="path">The web path to resolve.</param>
        /// <returns>A path with all relative path parts resolved.</returns>
        public static string ResolveRelativeWebPathParts(string path)
        {
            return ResolveRelativePathParts(path, WebDirectorySeparatorChar);
        }

        #endregion " Resolve Relative Path Parts Methods "

        #region " Private Methods "

        private static string[] GetPathParts(string path)
        {
            string[] parts = path.Split(new[] {DirectorySeparatorChar, AltDirectorySeparatorChar, WebDirectorySeparatorChar}, StringSplitOptions.None);
            return parts.Length < 1 ? null : parts;
        }

        private static void ValidatePath(string path)
        {
            ValidatePathImpl(path, true);
        }

        private static bool ValidatePathImpl(string path, bool throwOnInvalidPath)
        {
            ArgumentException argumentException =
                ValidationUtility.GetArgumentExceptionForStringIsNullOrIsEmptyOrContainsOnlyWhitespace(path, "path");
            if (null != argumentException)
            {
                if (throwOnInvalidPath)
                {
                    throw argumentException;
                }
                return false;
            }

            if (ContainsSpecifiedCharacters(path, _invalidPathCharacters))
            {
                if (throwOnInvalidPath)
                {
                    throw new ArgumentException("The path contains one or more of the invalid characters defined in the GetInvalidPathChars method.", "path");
                }
                return false;
            }

            return true;
        }

        #endregion " Private Methods "

        #endregion " Methods "
    }
}